diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-10-15 12:52:11 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-10-15 12:52:11 +0000 |
| commit | b54f6f03150dd78d86db62201b6386bf14b72394 (patch) | |
| tree | b3092bb34805fdc65eee5282e86a9fb90ba20d6e /app/[lng]/evcp/data-room/[projectId]/members/page.tsx | |
| parent | c1bd1a2f499ee2f0742170021b37dab410983ab7 (diff) | |
(대표님) 커버, 데이터룸, 파일매니저, 담당자할당 등
Diffstat (limited to 'app/[lng]/evcp/data-room/[projectId]/members/page.tsx')
| -rw-r--r-- | app/[lng]/evcp/data-room/[projectId]/members/page.tsx | 811 |
1 files changed, 811 insertions, 0 deletions
diff --git a/app/[lng]/evcp/data-room/[projectId]/members/page.tsx b/app/[lng]/evcp/data-room/[projectId]/members/page.tsx new file mode 100644 index 00000000..18442c0e --- /dev/null +++ b/app/[lng]/evcp/data-room/[projectId]/members/page.tsx @@ -0,0 +1,811 @@ +// app/projects/[projectId]/members/page.tsx +'use client'; + +import { use, useState, useEffect, useRef } from 'react'; +import { + Users, + UserPlus, + Crown, + Shield, + Eye, + Edit2, + Trash2, + Mail, + MoreVertical, + Search, + Filter, + Check, + ChevronsUpDown, + Loader2, + UserCog +} from 'lucide-react'; +import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'; +import { Button } from '@/components/ui/button'; +import { Input } from '@/components/ui/input'; +import { Badge } from '@/components/ui/badge'; +import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar'; +import { + Dialog, + DialogContent, + DialogDescription, + DialogFooter, + DialogHeader, + DialogTitle, +} from '@/components/ui/dialog'; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuSeparator, + DropdownMenuTrigger, +} from '@/components/ui/dropdown-menu'; +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from '@/components/ui/select'; +import { + Popover, + PopoverContent, + PopoverTrigger, +} from '@/components/ui/popover'; +import { + Command, + CommandEmpty, + CommandGroup, + CommandInput, + CommandItem, + CommandList, +} from '@/components/ui/command'; +import { Label } from '@/components/ui/label'; +import { useToast } from '@/hooks/use-toast'; +import { cn } from '@/lib/utils'; +import { + Table, TableBody, TableCaption, TableCell, TableHead, TableHeader, TableRow +} from '@/components/ui/table'; +import { Separator } from '@/components/ui/separator'; +import { getUsersForFilter } from '@/lib/gtc-contract/service'; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" + +interface Member { + id: string; + userId: number; + user: { + name: string; + email: string; + imageUrl?: string; + domain: string; + }; + role: 'owner' | 'admin' | 'editor' | 'viewer'; + addedAt: string; + lastAccess?: string; +} + +interface User { + id: number; + name: string; + email: string; + domain?: string; // 'partners' | 'internal' 등 +} + +export default function ProjectMembersPage({ + params: promiseParams +}: { + params: Promise<{ projectId: string }> +}) { + // Next.js 15+ params Promise 처리 + const params = use(promiseParams); + const projectId = params.projectId; + + const [members, setMembers] = useState<Member[]>([]); + const [loading, setLoading] = useState(true); + const [searchQuery, setSearchQuery] = useState(''); + const [roleFilter, setRoleFilter] = useState<string>('all'); + const [addMemberOpen, setAddMemberOpen] = useState(false); + const [editingMember, setEditingMember] = useState<Member | null>(null); + + // 사용자 선택 관련 상태 + const [availableUsers, setAvailableUsers] = useState<User[]>([]); + const [selectedUser, setSelectedUser] = useState<User | null>(null); + const [userSearchTerm, setUserSearchTerm] = useState(''); + const [userPopoverOpen, setUserPopoverOpen] = useState(false); + const [loadingUsers, setLoadingUsers] = useState(false); + const [isExternalUser, setIsExternalUser] = useState(false); // 외부 사용자 여부 + + const [newMemberRole, setNewMemberRole] = useState<string>('viewer'); + const [currentUserRole, setCurrentUserRole] = useState<string>('viewer'); + const [page, setPage] = useState(1); + const pageSize = 20; + + // Command component key management + const userOptionIdsRef = useRef<Record<number, string>>({}); + const popoverContentId = `popover-content-${Date.now()}`; + const commandId = `command-${Date.now()}`; + + const { toast } = useToast(); + + useEffect(() => { + setPage(1); + }, [searchQuery, roleFilter]); + + useEffect(() => { + fetchMembers(); + checkUserRole(); + }, [projectId]); + + // 다이얼로그가 열릴 때 사용자 목록 가져오기 + useEffect(() => { + if (addMemberOpen) { + fetchAvailableUsers(); + } else { + // 다이얼로그가 닫힐 때 초기화 + setSelectedUser(null); + setUserSearchTerm(''); + setNewMemberRole('viewer'); + setIsExternalUser(false); + } + }, [addMemberOpen]); + + const fetchAvailableUsers = async () => { + try { + setLoadingUsers(true); + const users = await getUsersForFilter(); + // 이미 프로젝트에 있는 멤버는 제외 + const memberUserIds = members.map(m => m.userId); + const filteredUsers = users.filter(u => !memberUserIds.includes(u.id)); + setAvailableUsers(filteredUsers); + } catch (error) { + console.error('사용자 목록 로드 실패:', error); + toast({ + title: '오류', + description: '사용자 목록을 불러올 수 없습니다.', + variant: 'destructive', + }); + } finally { + setLoadingUsers(false); + } + }; + + const fetchMembers = async () => { + try { + setLoading(true); + const response = await fetch(`/api/projects/${projectId}/members`); + const data = await response.json(); + setMembers(data.member); + } catch (error) { + toast({ + title: '오류', + description: '멤버 목록을 불러올 수 없습니다.', + variant: 'destructive', + }); + } finally { + setLoading(false); + } + }; + + const checkUserRole = async () => { + try { + const response = await fetch(`/api/projects/${projectId}/access`); + const data = await response.json(); + setCurrentUserRole(data.role); + } catch (error) { + console.error('권한 확인 실패:', error); + } + }; + + const addMember = async () => { + if (!selectedUser) { + toast({ + title: '오류', + description: '사용자를 선택해주세요.', + variant: 'destructive', + }); + return; + } + + try { + const response = await fetch(`/api/projects/${projectId}/members`, { + method: 'POST', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ + userId: selectedUser.id, + role: newMemberRole, + }), + }); + + if (!response.ok) throw new Error('멤버 추가 실패'); + + toast({ + title: '성공', + description: '새 멤버가 추가되었습니다.', + }); + + setAddMemberOpen(false); + fetchMembers(); + } catch (error) { + toast({ + title: '오류', + description: '멤버 추가에 실패했습니다.', + variant: 'destructive', + }); + } + }; + + const updateMemberRole = async (memberId: string, newRole: string) => { + try { + const response = await fetch(`/api/projects/${projectId}/members/${memberId}`, { + method: 'PATCH', + headers: { 'Content-Type': 'application/json' }, + body: JSON.stringify({ role: newRole }), + }); + + if (!response.ok) throw new Error('역할 변경 실패'); + + toast({ + title: '성공', + description: '멤버 역할이 변경되었습니다.', + }); + + fetchMembers(); + } catch (error) { + toast({ + title: '오류', + description: '역할 변경에 실패했습니다.', + variant: 'destructive', + }); + } + }; + + const removeMember = async (memberId: string) => { + try { + const response = await fetch(`/api/projects/${projectId}/members/${memberId}`, { + method: 'DELETE', + }); + + if (!response.ok) throw new Error('멤버 제거 실패'); + + toast({ + title: '성공', + description: '멤버가 제거되었습니다.', + }); + + fetchMembers(); + } catch (error) { + toast({ + title: '오류', + description: '멤버 제거에 실패했습니다.', + variant: 'destructive', + }); + } + }; + + const handleSelectUser = (user: User) => { + setSelectedUser(user); + setUserPopoverOpen(false); + + // 외부 사용자(partners)인 경우 역할을 viewer로 고정 + if (user.domain === 'partners') { + setIsExternalUser(true); + setNewMemberRole('viewer'); + } else { + setIsExternalUser(false); + // 내부 사용자는 기본값 viewer로 설정하되 변경 가능 + setNewMemberRole('viewer'); + } + }; + + const formatDateShort = (iso?: string) => + iso ? new Date(iso).toLocaleDateString() : '-'; + + const roleConfig = { + owner: { label: 'Owner', icon: Crown, color: 'text-yellow-500', bg: 'bg-yellow-50' }, + admin: { label: 'Admin', icon: Shield, color: 'text-blue-500', bg: 'bg-blue-50' }, + editor: { label: 'Editor', icon: Edit2, color: 'text-green-500', bg: 'bg-green-50' }, + viewer: { label: 'Viewer', icon: Eye, color: 'text-gray-500', bg: 'bg-gray-50' }, + }; + + const filteredMembers = members.filter(member => { + const matchesSearch = member.user.name.toLowerCase().includes(searchQuery.toLowerCase()) || + member.user.email.toLowerCase().includes(searchQuery.toLowerCase()); + const matchesRole = roleFilter === 'all' || member.role === roleFilter; + return matchesSearch && matchesRole; + }); + + // 사용자 검색 필터링 + const filteredUsers = availableUsers.filter(user => + user.name.toLowerCase().includes(userSearchTerm.toLowerCase()) || + user.email.toLowerCase().includes(userSearchTerm.toLowerCase()) + ); + + const canManageMembers = currentUserRole === 'owner' || currentUserRole === 'admin'; + + const totalPages = Math.max(1, Math.ceil(filteredMembers.length / pageSize)); + const paginatedMembers = filteredMembers.slice((page - 1) * pageSize, page * pageSize); + + if (loading) { + return ( + <div className="flex items-center justify-center min-h-[400px]"> + <div className="text-center space-y-3"> + <Loader2 className="h-8 w-8 animate-spin text-primary mx-auto" /> + <p className="text-sm text-muted-foreground">멤버 목록을 불러오는 중...</p> + </div> + </div> + ); + } + + return ( + <div className="p-6 space-y-6"> + {/* 헤더 */} + <div className="flex items-center justify-between"> + <div> + <h1 className="text-2xl font-bold">프로젝트 멤버</h1> + <p className="text-muted-foreground mt-1"> + 프로젝트에 참여 중인 멤버를 관리합니다 + </p> + </div> + + {canManageMembers && ( + <Button onClick={() => setAddMemberOpen(true)}> + <UserPlus className="h-4 w-4 mr-2" /> + 멤버 추가 + </Button> + )} + </div> + + {/* 필터 */} + <div className="flex items-center gap-3"> + <div className="relative flex-1 max-w-md"> + <Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-muted-foreground" /> + <Input + placeholder="이름 또는 이메일로 검색..." + className="pl-9" + value={searchQuery} + onChange={(e) => setSearchQuery(e.target.value)} + /> + </div> + + <Select value={roleFilter} onValueChange={setRoleFilter}> + <SelectTrigger className="w-40"> + <SelectValue /> + </SelectTrigger> + <SelectContent> + <SelectItem value="all">모든 역할</SelectItem> + <SelectItem value="owner">Owner</SelectItem> + <SelectItem value="admin">Admin</SelectItem> + <SelectItem value="editor">Editor</SelectItem> + <SelectItem value="viewer">Viewer</SelectItem> + </SelectContent> + </Select> + </div> + + {/* 멤버 목록 (Table) */} + <div className="overflow-x-auto"> + <Table className="[&_td]:py-2 [&_th]:py-2 text-sm"> + <TableHeader className="sticky top-0 bg-background z-10"> + <TableRow> + <TableHead className="w-[44px]"></TableHead> + <TableHead className="w-[100px]">이름</TableHead> + <TableHead className="min-w-[150px]">이메일</TableHead> + <TableHead className="w-[90px] text-center">구분</TableHead> + <TableHead className="w-[140px]">역할</TableHead> + <TableHead className="w-[130px]">추가일</TableHead> + <TableHead className="w-[150px]">마지막 접속</TableHead> + <TableHead className="w-[60px] text-right">액션</TableHead> + </TableRow> + </TableHeader> + + <TableBody> + {paginatedMembers.length > 0 ? ( + paginatedMembers.map((member) => { + const config = roleConfig[member.role]; + const Icon = config.icon; + const isInternal = member.user.domain !== 'partners'; + + return ( + <TableRow key={member.id} className="hover:bg-accent/40"> + {/* Avatar */} + <TableCell className="align-middle"> + <Avatar className="h-8 w-8"> + <AvatarImage src={member.user.imageUrl} /> + <AvatarFallback> + {member.user.name?.charAt(0).toUpperCase()} + </AvatarFallback> + </Avatar> + </TableCell> + + {/* Name */} + <TableCell className="align-middle"> + <span className="font-medium">{member.user.name}</span> + </TableCell> + + {/* Email */} + <TableCell className="align-middle"> + <span className="text-muted-foreground">{member.user.email}</span> + </TableCell> + + {/* Domain */} + <TableCell className="align-middle text-center"> + <Badge variant={isInternal ? 'secondary' : 'outline'}> + {isInternal ? 'Internal' : 'Partner'} + </Badge> + </TableCell> + + {/* Role */} + <TableCell className="align-middle"> + {canManageMembers && member.role !== 'owner' && member.user.domain !== 'partners' ? ( + <Select + value={member.role} + onValueChange={(v) => updateMemberRole(member.id, v)} + > + <SelectTrigger className="h-8 w-[120px]"> + <div className={cn('flex items-center gap-1')}> + <Icon className={cn('h-3 w-3', config.color)} /> + <span className={cn('text-xs font-medium')}> + {config.label} + </span> + </div> + </SelectTrigger> + <SelectContent> + <SelectItem value="viewer">Viewer</SelectItem> + <SelectItem value="editor">Editor</SelectItem> + <SelectItem value="admin">Admin</SelectItem> + </SelectContent> + </Select> + ) : ( + <div className="inline-flex items-center gap-2"> + <div className={cn('px-2 py-1 rounded-full inline-flex items-center gap-1', config.bg)}> + <Icon className={cn('h-3 w-3', config.color)} /> + <span className={cn('text-xs font-medium', config.color)}> + {config.label} + </span> + </div> + {member.user.domain === 'partners' && canManageMembers && member.role !== 'owner' && ( + <span className="text-xs text-muted-foreground">(고정)</span> + )} + </div> + )} + </TableCell> + + {/* AddedAt */} + <TableCell className="align-middle"> + {formatDateShort(member.addedAt)} + </TableCell> + + {/* LastAccess */} + <TableCell className="align-middle"> + {formatDateShort(member.lastAccess)} + </TableCell> + + {/* Actions */} + <TableCell className="align-middle"> + <div className="flex justify-end"> + {canManageMembers && member.role !== 'owner' ? ( + <DropdownMenu> + <DropdownMenuTrigger asChild> + <Button variant="ghost" size="icon"> + <MoreVertical className="h-4 w-4" /> + </Button> + </DropdownMenuTrigger> + <DropdownMenuContent align="end"> + <DropdownMenuItem> + <Mail className="h-4 w-4 mr-2" /> + 메일 보내기 + </DropdownMenuItem> + <DropdownMenuSeparator /> + <DropdownMenuItem + className="text-red-600" + onClick={() => removeMember(member.id)} + > + <Trash2 className="h-4 w-4 mr-2" /> + 제거 + </DropdownMenuItem> + </DropdownMenuContent> + </DropdownMenu> + ) : ( + <Button variant="ghost" size="icon" disabled> + <MoreVertical className="h-4 w-4" /> + </Button> + )} + </div> + </TableCell> + </TableRow> + ); + }) + ) : ( + <TableRow> + <TableCell colSpan={8} className="h-32 text-center text-muted-foreground"> + <div className="flex flex-col items-center justify-center gap-2"> + <Users className="h-8 w-8 text-muted-foreground/60" /> + <span>검색 결과가 없습니다</span> + </div> + </TableCell> + </TableRow> + )} + </TableBody> + </Table> + </div> + + {/* Pagination */} + <div className="flex items-center justify-between px-4 py-3 border-t"> + <div className="text-sm text-muted-foreground"> + 총 {filteredMembers.length}명 · {pageSize}명/페이지 + </div> + <div className="flex items-center gap-2"> + <Button + variant="outline" + size="sm" + onClick={() => setPage((p) => Math.max(1, p - 1))} + disabled={page === 1} + > + 이전 + </Button> + <span className="text-sm"> + {page} / {totalPages} + </span> + <Button + variant="outline" + size="sm" + onClick={() => setPage((p) => Math.min(totalPages, p + 1))} + disabled={page === totalPages} + > + 다음 + </Button> + </div> + </div> + + {/* 멤버 추가 다이얼로그 */} + <Dialog open={addMemberOpen} onOpenChange={setAddMemberOpen}> + <DialogContent className="max-w-lg"> + <DialogHeader> + <DialogTitle>멤버 추가</DialogTitle> + <DialogDescription> + 프로젝트에 멤버를 추가합니다 + </DialogDescription> + </DialogHeader> + + <Tabs defaultValue="internal" className="w-full"> + <TabsList className="grid w-full grid-cols-2"> + <TabsTrigger value="internal">내부 사용자</TabsTrigger> + <TabsTrigger value="external" className="flex items-center gap-2"> + 외부 사용자 + <Badge variant="outline" className="ml-1 text-xs">Viewer 전용</Badge> + </TabsTrigger> + </TabsList> + + <TabsContent value="internal" className="space-y-4 mt-4"> + <div className="space-y-2"> + <Label htmlFor="internal-user">사용자 선택</Label> + + {loadingUsers ? ( + <div className="flex items-center justify-center py-4"> + <Loader2 className="h-4 w-4 animate-spin" /> + <span className="ml-2 text-sm text-muted-foreground">사용자 목록 불러오는 중...</span> + </div> + ) : ( + <> + <Popover open={userPopoverOpen} onOpenChange={setUserPopoverOpen}> + <PopoverTrigger asChild> + <Button + variant="outline" + role="combobox" + aria-expanded={userPopoverOpen} + className="w-full justify-between" + > + <span className="truncate"> + {selectedUser && selectedUser.domain !== 'partners' ? ( + <div className="text-left"> + <div className="font-medium">{selectedUser.name}</div> + <div className="text-xs text-muted-foreground">{selectedUser.email}</div> + </div> + ) : ( + "내부 사용자를 선택하세요..." + )} + </span> + <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> + </Button> + </PopoverTrigger> + <PopoverContent className="w-[460px] p-0"> + <Command> + <CommandInput + placeholder="이름 또는 이메일로 검색..." + value={userSearchTerm} + onValueChange={setUserSearchTerm} + /> + <CommandList + className="max-h-[300px]" + onWheel={(e) => { + e.stopPropagation(); + const target = e.currentTarget; + target.scrollTop += e.deltaY; + }} + > + <CommandEmpty>사용자를 찾을 수 없습니다.</CommandEmpty> + <CommandGroup heading="내부 사용자 목록"> + {filteredUsers + .filter(u => u.domain !== 'partners') + .map((user) => ( + <CommandItem + key={user.id} + onSelect={() => { + setSelectedUser(user); + setUserPopoverOpen(false); + setIsExternalUser(false); + setNewMemberRole('viewer'); + }} + value={`${user.name} ${user.email}`} + className="truncate" + > + <Users className="mr-2 h-4 w-4 text-blue-500 flex-shrink-0" /> + <div className="flex-1 truncate"> + <div className="font-medium truncate">{user.name}</div> + <div className="text-xs text-muted-foreground truncate">{user.email}</div> + </div> + <Check + className={cn( + "ml-2 h-4 w-4 flex-shrink-0", + selectedUser?.id === user.id && !isExternalUser ? "opacity-100" : "opacity-0" + )} + /> + </CommandItem> + ))} + </CommandGroup> + </CommandList> + </Command> + </PopoverContent> + </Popover> + + <p className="text-xs text-muted-foreground"> + 내부 사용자는 모든 역할을 부여할 수 있습니다. + </p> + </> + )} + </div> + + <div className="space-y-2"> + <Label htmlFor="internal-role">역할</Label> + <Select + value={newMemberRole} + onValueChange={setNewMemberRole} + disabled={!selectedUser || isExternalUser} + > + <SelectTrigger id="internal-role"> + <SelectValue /> + </SelectTrigger> + <SelectContent> + <SelectItem value="viewer">Viewer - 읽기 전용</SelectItem> + <SelectItem value="editor">Editor - 파일 편집 가능</SelectItem> + <SelectItem value="admin">Admin - 프로젝트 관리</SelectItem> + </SelectContent> + </Select> + </div> + </TabsContent> + + <TabsContent value="external" className="space-y-4 mt-4"> + <div className="rounded-lg bg-amber-50 border border-amber-200 p-3 mb-4"> + <p className="text-sm text-amber-800"> + <strong>보안 정책 안내</strong><br/> + 외부 사용자(파트너)는 보안 정책상 Viewer 권한만 부여 가능합니다. + </p> + </div> + + <div className="space-y-2"> + <Label htmlFor="external-user">파트너 선택</Label> + + {loadingUsers ? ( + <div className="flex items-center justify-center py-4"> + <Loader2 className="h-4 w-4 animate-spin" /> + <span className="ml-2 text-sm text-muted-foreground">사용자 목록 불러오는 중...</span> + </div> + ) : ( + <Popover open={userPopoverOpen} onOpenChange={setUserPopoverOpen}> + <PopoverTrigger asChild> + <Button + variant="outline" + role="combobox" + aria-expanded={userPopoverOpen} + className="w-full justify-between" + > + <span className="truncate"> + {selectedUser && selectedUser.domain === 'partners' ? ( + <span className="flex items-center gap-2"> + {selectedUser.name} + <Badge variant="outline" className="ml-1 text-xs">외부</Badge> + </span> + ) : ( + "외부 사용자를 선택하세요..." + )} + </span> + <ChevronsUpDown className="ml-2 h-4 w-4 shrink-0 opacity-50" /> + </Button> + </PopoverTrigger> + <PopoverContent className="w-[460px] p-0"> + <Command> + <CommandInput + placeholder="이름으로 검색..." + value={userSearchTerm} + onValueChange={setUserSearchTerm} + /> + <CommandList + className="max-h-[300px]" + onWheel={(e) => { + e.stopPropagation(); + const target = e.currentTarget; + target.scrollTop += e.deltaY; + }} + > + <CommandEmpty>파트너를 찾을 수 없습니다.</CommandEmpty> + <CommandGroup heading="파트너 목록"> + {filteredUsers + .filter(u => u.domain === 'partners') + .map((user) => ( + <CommandItem + key={user.id} + onSelect={() => { + setSelectedUser(user); + setUserPopoverOpen(false); + setIsExternalUser(true); + setNewMemberRole('viewer'); + }} + value={user.name} + className="truncate" + > + <Users className="mr-2 h-4 w-4 text-amber-600" /> + <span className="truncate flex-1">{user.name}</span> + <Badge variant="outline" className="text-xs mx-2">파트너</Badge> + <Check + className={cn( + "ml-auto h-4 w-4 flex-shrink-0", + selectedUser?.id === user.id && isExternalUser ? "opacity-100" : "opacity-0" + )} + /> + </CommandItem> + ))} + </CommandGroup> + </CommandList> + </Command> + </PopoverContent> + </Popover> + )} + </div> + + <div className="space-y-2"> + <Label htmlFor="external-role">역할</Label> + <Select value="viewer" disabled> + <SelectTrigger id="external-role" className="opacity-60"> + <SelectValue /> + </SelectTrigger> + <SelectContent> + <SelectItem value="viewer">Viewer - 읽기 전용 (고정)</SelectItem> + </SelectContent> + </Select> + </div> + </TabsContent> + </Tabs> + + <DialogFooter> + <Button + variant="outline" + onClick={() => { + setAddMemberOpen(false); + setSelectedUser(null); + setUserSearchTerm(''); + setNewMemberRole('viewer'); + setIsExternalUser(false); + }} + > + 취소 + </Button> + <Button + onClick={addMember} + disabled={!selectedUser} + > + 추가하기 + </Button> + </DialogFooter> + </DialogContent> + </Dialog> + </div> + ); +}
\ No newline at end of file |
